import datetime
import functools
import requests
import base64
from jose import jwt
from package.fastapi import security
from model.model import ApiKey, OIDCConfigure
from package.fastapi.cache import apikey_cache, token_cache
from builtin.role import THIRD_ROLE
from router.user.view import User, RoleUserView
from fastapi import HTTPException
from common.cache import configure_cache, fail_auth_cache, temp_userlock_cache, temp_iplock_cache


class LoginOptionChecker:
    """登陆安全"""

    def __init__(self):
        login_option = configure_cache.get('login_option').get('config', {})
        black_ip = configure_cache.get('black_ip').get('config', {})

        self.lock_window = login_option.get('lock_window', 60)
        self.lock_time = login_option.get('lock_time', 180)
        self.max_retry_times = login_option.get('max_retry_times', 3)
        self.if_lock_user = login_option.get('lock_user', True)
        self.if_lock_ip = login_option.get('lock_ip', False)
        self.alert_report = login_option.get('alert_report', False)
        self.black_ip = black_ip.get('black_list', [])
        self.current_ts = int(datetime.datetime.utcnow().timestamp() * 1000)

        self.locked_user = temp_userlock_cache.get()
        self.locked_ip = temp_iplock_cache.get()

    def is_temp_lock_user(self, name):
        return name in self.locked_user

    def is_temp_lock_ip(self, ip):
        return ip in self.locked_ip

    def is_black_ip(self, ip):
        return ip in self.black_ip

    def is_max_retry_times(self, name):
        times = fail_auth_cache.set(name, self.lock_window)
        if times >= self.max_retry_times:
            return True
        return False

    def lock_user(self, name):
        if self.if_lock_user:
            temp_userlock_cache.set(name, current_ts=self.current_ts, lock_ts=self.lock_time * 1000)

    def lock_ip(self, ip):
        if self.if_lock_ip:
            temp_iplock_cache.set(ip, current_ts=self.current_ts, lock_ts=self.lock_time * 1000)


class OidcAuth:
    """oidc 认证流程"""

    Password = 'Oidc.123456'
    
    def __init__(self, name, client_id, client_secret, access_token_url, verify_url, redirect_url, cert,
                 userid_field='preferred_username', username_field: str = 'name'):
        self.name = name
        self.client_id = client_id
        self.client_secret = client_secret
        self.access_token_url = access_token_url
        self.verify_url = verify_url
        self.redirect_url = redirect_url
        self.cert = cert
        self.userid_field = userid_field
        self.username_field = username_field

    def _verify_oidc_token(self, token: dict):
        if not self.verify_url:
            return

        basic_auth_data = f'{self.client_id}:{self.client_secret}'.encode('utf-8')
        header = {'Content-Type': 'application/x-www-form-urlencoded',
                  'Authorization': 'Basic ' + base64.b64encode(basic_auth_data).decode('UTF-8')}
        data = {
            'token_type_hint': 'access_token',
            'token': token['access_token']}
        resp = requests.post(url=self.verify_url, data=data, headers=header)
        if resp.status_code != 200:
            raise HTTPException(status_code=400, detail='Token校验失败')

        verify = resp.json()
        if 'active' not in verify:
            return
        if not verify.get('active'):
            raise HTTPException(status_code=400, detail='用户已被禁用')
    
    def _get_oidc_token(self, code: str) -> str:
        data = {
            'grant_type': 'authorization_code',
            'client_id': self.client_id,
            'client_secret': self.client_secret,
            'code': code,
            'redirect_uri': self.redirect_url
        }
        resp = requests.post(url=self.access_token_url, data=data,
                             headers={"Content-Type": "application/x-www-form-urlencoded"})
        if resp.status_code != 200:
            raise HTTPException(status_code=400, detail='OIDC Token获取异常')

        token = resp.json()
        self._verify_oidc_token(token)
        return token["access_token"]

    def _get_oidc_user(self, access_token):
        option = {'verify_signature': True if self.cert else False, 'verify_aud': False}
        decoded = jwt.decode(access_token, self.cert, audience=self.client_id, options=option)

        if self.username_field and self.username_field not in decoded:
            raise HTTPException(status_code=400, detail='用户ID字段不存在')
        if self.username_field and self.username_field not in decoded:
            raise HTTPException(status_code=400, detail='用户名不存在')
        return {
            'name': f'{decoded[self.username_field]}',
            'alias': decoded[self.username_field],
            'password': self.Password,
            'extra': {'OIDC': self.name}
            }

    def get_user_by_code(self, code):
        token = self._get_oidc_token(code)
        return self._get_oidc_user(token)


class AuthView(User):

    @classmethod
    def post_token(cls, session, name, password, request):
        request.state.user = {'name': name}  # 审计记录当前登陆人

        item = cls.get_item(session, filter_=[cls.name == name])
        if not item:
            raise HTTPException(status_code=400, detail='账号不存在')
        if not item.enabled:
            raise HTTPException(status_code=400, detail='账号已被禁用')
        decrypt_password = security.rsa.decrypt(password)
        password = security.hash_.calculate(decrypt_password)
        if item.password != password:
            raise HTTPException(status_code=400, detail='账号密码不匹配')
        token = security.jwt.encode({'id': item.id, 'name': item.name})
        token_cache.set(item.id, user=item)
        return {"access_token": token, "token_type": "bearer", "user": item}

    @staticmethod
    def auth_safe(func):
        """登陆环境安全检查"""
        @functools.wraps(func)
        def deco(*args, **kwargs):
            auth_param = kwargs.get('form') or kwargs.get('body')
            name = getattr(auth_param, 'username', None) or getattr(auth_param, 'name', None)
            request = kwargs['request']

            ip = (request.headers.get('X-Real-IP') or request.headers.get('X-Forwarded-For') or request.client.host)
            lsc = LoginOptionChecker()

            if lsc.is_temp_lock_user(name):
                detail = '账号已被锁定, 请稍后再试'
                raise HTTPException(400, detail=detail)

            if lsc.is_temp_lock_ip(ip):
                detail = 'IP已被锁定, 请稍后再试'
                raise HTTPException(400, detail=detail)

            if lsc.is_black_ip(ip):
                detail = 'IP已被加入黑名单'
                raise HTTPException(400, detail=detail)

            try:
                resp = func(*args, **kwargs)
                return resp

            except HTTPException as e:
                if lsc.is_max_retry_times(name):
                    lsc.lock_user(name)
                    lsc.lock_ip(ip)

                detail = e.detail
                raise HTTPException(400, detail=detail)

            except Exception as e:
                detail = '认证失败'
                raise HTTPException(400, detail=detail)

        return deco


class ApikeyView(ApiKey):

    @classmethod
    def create_apikey(cls, session, body):
        item = cls.create_item(session, body)
        apikey_cache.set(item.id,
                         user=User.get_item(session, filter_=[User.id == '00000000000000000000000000000001']),
                         expired_at=body['expired_at'])
        return item

    @classmethod
    def update_apikey(cls, session, body, id):
        item = cls.update_item(session, filter_=[cls.id == id], update=body)
        apikey_cache.set(item.id, body['expired_at'])
        return item

    @classmethod
    def delete_apikey(cls, session, ids):
        """apikey"""
        items = cls.get_items(session, filter_=[cls.id.in_(ids)])
        for item in items:
            apikey_cache.delete(item.id)  # 缓存清除
        return cls.delete_items(session, filter_=[cls.id.in_(ids)])

    @classmethod
    def load_database2cache(cls, session):
        """设置缓存"""
        items = cls.get_items(session)
        for item in items:
            apikey_cache.set(item.id,
                             user=User.get_item(session, filter_=[User.id == '00000000000000000000000000000001']),
                             expired_at=item.expired_at)


class OIDCView(OIDCConfigure):
    @classmethod
    def oidc_sign(cls, session, state):
        item = cls.get_item(session, filter_=[cls.state == state])
        url = f'{item.authorization_url}?' \
              f'client_id={item.client_id}&' \
              f'redirect_uri={item.redirect_url}&' \
              f'response_type=code&' \
              f'scope=openid%20profile&' \
              f'state={state}'
        return url

    @classmethod
    def oidc_callback(cls, session, code, state, request):
        item = cls.get_item(session, filter_=[cls.state == state])
        if not item:
            raise HTTPException(status_code=404, detail='OIDC未配置')

        oidc_auth = OidcAuth(item.state, item.client_id, item.client_secret, item.token_url, item.verify_url,
                             item.redirect_url, item.cert, item.userid_field, item.username_field)

        oidc_user = oidc_auth.get_user_by_code(code)

        item = User.get_item(session, filter_=[User.name == oidc_user['name']])
        if not item:
            # 添加用户
            item = User.create_item(session, oidc_user)
            # 绑定角色
            RoleUserView.bind_role_users(session, id=THIRD_ROLE['id'], user_ids=[item.id])
        # 返回token
        return AuthView.post_token(session, item.name, item.password, request)
